EgwApp   F
last analyzed

Complexity

Total Complexity 179

Size/Duplication

Total Lines 2049
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1216
dl 0
loc 2049
rs 0.8
c 0
b 0
f 0
wmc 179

42 Functions

Rating   Name   Duplication   Size   Complexity  
B delete_favorite 0 62 4
A _mailvelopeDomain 0 11 2
F setState 0 82 16
A observer 0 22 1
A egwTutorialGetData 0 18 1
A uid 0 9 1
A handle_dropped_mail 0 45 4
A push 0 28 3
A share_merge 0 29 4
A tutorial_autoloadDiscard 0 13 2
B mailvelopeCreateBackupRestoreDialog 0 65 3
A is_share_enabled 0 12 1
A mailvelopeDeleteBackup 0 21 2
A mailvelopeCreateBackupDialog 0 42 1
A getWindowTitle 0 17 2
B _share_link_callback 0 32 6
B mailvelopeSyncHandler 0 99 2
B destroy 0 12 6
A _refresh_fav_nm 0 23 3
B egwTutorial_init 0 68 6
A open 0 13 1
A _do_action 0 3 1
B share_link 0 35 6
A _set_Window_title 0 11 2
C mailvelopeGetCheckRecipients 0 72 8
B mailvelopeInstallationOffer 0 62 6
F highlight_favorite 0 112 22
A _fix_iFrameScrolling 0 23 3
C viewEntry 0 115 7
A egwTutorialPopup 0 10 1
B action 0 52 8
A mailvelopeCreateRestoreDialog 0 38 1
A et2_ready 0 20 3
C _init_sidebox 0 91 9
A mailvelopeAvailable 0 20 2
F _create_favorite_popup 0 161 11
A updateList 0 22 2
A _mailvelopeBackupFileOperator 0 39 2
A tutorial_videoOnClick 0 12 2
A add_favorite 0 72 4
C mailvelopeOpenKeyring 0 103 6
A getState 0 27 2

How to fix   Complexity   

Complexity

Complex classes like EgwApp often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * EGroupware clientside Application javascript base object
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Ralf Becker <[email protected]>
9
 * @author Hadi Nategh <[email protected]>
10
 * @author Nathan Gray <[email protected]>
11
 */
12
13
import 'jquery';
14
import 'jqueryui';
15
import '../jsapi/egw_global';
16
import '../etemplate/et2_types';
17
18
/**
19
 * Type for push-message
20
 */
21
export interface PushData
22
{
23
	type: "add"|"edit"|"update"|"delete"|"unknown";
24
	app: string;	// app-name, can include a subtype eg. "projectmanager-element"
25
	id: string | number;
26
	acl?: any;	// app-specific acl data, eg. the owner, or array of participants
27
	account_id: number;	// user that caused the change
28
	[propName:string]: any;	// arbitrary more parameters
29
}
30
31
/**
32
 * Common base class for application javascript
33
 * Each app should extend as needed.
34
 *
35
 * All application javascript should be inside.  Intitialization goes in init(),
36
 * clean-up code goes in destroy().  Initialization is done once all js is loaded.
37
 *
38
 * var app.appname = AppJS.extend({
39
 *	// Actually set this one, the rest is example
40
 *	appname: appname,
41
 *
42
 *	internal_var: 1000,
43
 *
44
 *	init: function()
45
 *	{
46
 *		// Call the super
47
 *		this._super.apply(this, arguments);
48
 *
49
 *		// Init the stuff
50
 *		if ( egw.preference('dateformat', 'common') )
51
 *		{
52
 *			// etc
53
 *		}
54
 *	},
55
 *	_private: function()
56
 *	{
57
 *		// Underscore private by convention
58
 *	}
59
 * });
60
 */
61
export abstract class EgwApp
62
{
63
	/**
64
	 * Internal application name - override this
65
	 */
66
	readonly appname: string;
67
68
	/**
69
	 * Internal reference to the most recently loaded etemplate2 widget tree
70
	 *
71
	 * NOTE: This variable can change which etemplate it points to as the user
72
	 * works.  For example, loading the home or admin apps can cause
73
	 * et2_ready() to be called again with a different template.  this.et2 will
74
	 * then point to a different template.  If the user then closes that tab,
75
	 * this.et2 will point to a destroyed object, and trying to use it will fail.
76
	 *
77
	 * If you need a reference to a certain template you can either store a local
78
	 * reference or access it through etemplate2.
79
	 *
80
	 * @example <caption>Store a local reference</caption>
81
	 *	// in et2_ready()
82
	 *	if(name == 'index') this.index_et2 = et2.widgetContainer;
83
	 *
84
	 *	// Remember to clean up in destroy()
85
	 *	delete this.index_et2;
86
	 *
87
	 *	// Instead of this.et2, using a local reference
88
	 *	this.index_et2 ...
89
	 *
90
	 *
91
	 * @example <caption>Access via etemplate2 object</caption>
92
	 * // Instead of this.et2, using it's unique ID
93
	 * var et2 = etemplate2.getById('myapp-index)
94
	 * if(et2)
95
	 * {
96
	 *		et2.widgetContainer. ...
97
	 * }
98
	 *
99
	 * @var {et2_container}
100
	 */
101
	et2: any;
102
103
	/**
104
	 * Internal reference to egw client-side api object for current app and window
105
	 *
106
	 * @var {egw}
107
	 */
108
	egw: IegwAppLocal;
109
110
	sidebox: JQuery;
111
112
	viewContainer: JQuery;
113
	viewTemplate: JQuery;
114
	et2_view: any;
115
	favorite_popup : JQuery | any;
116
117
	tutorial_initialised: boolean;
118
119
	dom_id : string;
120
121
	mailvelopeSyncHandlerObj : any;
122
123
	/**
124
	 * Initialization and setup goes here, but the etemplate2 object
125
	 * is not yet ready.
126
	 */
127
	constructor()
128
	{
129
		this.egw = egw(this.appname, window);
130
131
		// Initialize sidebox for non-popups.
132
		// ID set server side
133
		if(!this.egw.is_popup())
134
		{
135
			var sidebox = jQuery('#favorite_sidebox_'+this.appname);
136
			if(sidebox.length == 0 && egw_getFramework() != null)
137
			{
138
				var egw_fw = egw_getFramework();
139
				sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv);
140
			}
141
			// Make sure we're running in the top window when we init sidebox
142
			//@ts-ignore
143
			if(window.app[this.appname] === this && window.top.app[this.appname] !== this && window.top.app[this.appname])
144
			{
145
				//@ts-ignore
146
				window.top.app[this.appname]._init_sidebox(sidebox);
147
			}
148
			else
149
			{
150
				this._init_sidebox(sidebox);
151
			}
152
		}
153
		this.mailvelopeSyncHandlerObj = this.mailvelopeSyncHandler();
154
	}
155
156
	/**
157
	 * Clean up any created objects & references
158
	 * @param {object} _app local app object
159
	 */
160
	destroy(_app)
161
	{
162
		delete this.et2;
163
		if (this.sidebox)
164
			this.sidebox.off();
165
		delete this.sidebox;
166
		if (!_app) delete app[this.appname];
167
	}
168
169
	/**
170
	 * This function is called when the etemplate2 object is loaded
171
	 * and ready.  If you must store a reference to the et2 object,
172
	 * make sure to clean it up in destroy().  Note that this can be called
173
	 * several times, with different et2 objects, as templates are loaded.
174
	 *
175
	 * @param {etemplate2} et2
176
	 * @param {string} name template name
177
	 */
178
	et2_ready(et2, name : string)
179
	{
180
		if(this.et2 !== null)
181
		{
182
			egw.debug('log', "Changed et2 object");
183
		}
184
		this.et2 = et2.widgetContainer;
185
		this._fix_iFrameScrolling();
186
		if (this.egw && this.egw.is_popup()) this._set_Window_title();
187
188
		// Highlights the favorite based on initial list state
189
		this.highlight_favorite();
190
	}
191
192
	/**
193
	 * Observer method receives update notifications from all applications
194
	 *
195
	 * App is responsible for only reacting to "messages" it is interested in!
196
	 *
197
	 * @param {string} _msg message (already translated) to show, eg. 'Entry deleted'
198
	 * @param {string} _app application name
199
	 * @param {(string|number)} _id id of entry to refresh or null
200
	 * @param {string} _type either 'update', 'edit', 'delete', 'add' or null
201
	 * - update: request just modified data from given rows.  Sorting is not considered,
202
	 *		so if the sort field is changed, the row will not be moved.
203
	 * - edit: rows changed, but sorting may be affected.  Requires full reload.
204
	 * - delete: just delete the given rows clientside (no server interaction neccessary)
205
	 * - add: requires full reload for proper sorting
206
	 * @param {string} _msg_type 'error', 'warning' or 'success' (default)
207
	 * @param {object|null} _links app => array of ids of linked entries
208
	 * or null, if not triggered on server-side, which adds that info
209
	 * @return {false|*} false to stop regular refresh, thought all observers are run
210
	 */
211
	observer(_msg, _app, _id, _type, _msg_type, _links)
212
	{
213
214
	}
215
216
	/**
217
	 * Handle a push notification about entry changes from the websocket
218
	 *
219
	 * Get's called for data of all apps, but should only handle data of apps it displays,
220
	 * which is by default only it's own, but can be for multiple apps eg. for calendar.
221
	 *
222
	 * @param  pushData
223
	 * @param {string} pushData.app application name
224
	 * @param {(string|number)} pushData.id id of entry to refresh or null
225
	 * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null
226
	 * - update: request just modified data from given rows.  Sorting is not considered,
227
	 *		so if the sort field is changed, the row will not be moved.
228
	 * - edit: rows changed, but sorting may be affected.  Requires full reload.
229
	 * - delete: just delete the given rows clientside (no server interaction neccessary)
230
	 * - add: requires full reload for proper sorting
231
	 * @param {object|null} pushData.acl Extra data for determining relevance.  eg: owner or responsible to decide if update is necessary
232
	 * @param {number} pushData.account_id User that caused the notification
233
	 */
234
	push(pushData : PushData)
235
	{
236
		// don't care about other apps data, reimplement if your app does care eg. calendar
237
		if (pushData.app !== this.appname) return;
238
239
		// only handle delete by default, for simple case of uid === "$app::$id"
240
		if (pushData.type === 'delete')
241
		{
242
			egw.dataStoreUID(this.uid(pushData), null);
243
		}
244
	}
245
246
	/**
247
	 * Get (possible) app-specific uid
248
	 *
249
	 * @param {object} pushData see push method for individual attributes
250
	 */
251
	uid(pushData)
252
	{
253
		return pushData.app + '::' + pushData.id;
254
	}
255
256
	/**
257
	 * Method called after apps push implementation checked visibility
258
	 *
259
	 * @param {et2_nextmatch} nm
260
	 * @param pushData see push method for individual attributes
261
	 * @todo implement better way to update nextmatch widget without disturbing the user / state
262
	 * @todo show indicator that an update has happend
263
	 * @todo rate-limit update frequency
264
	 */
265
	updateList(nm, pushData : PushData)
266
	{
267
		switch (pushData.type)
268
		{
269
			case 'add':
270
			case 'unknown':
271
				nm.applyFilters();
272
				break;
273
274
			default:
275
				egw.dataRefreshUID(this.uid(pushData));
276
				break;
277
		}
278
	}
279
280
	/**
281
	 * Open an entry.
282
	 *
283
	 * Designed to be used with the action system as a callback
284
	 * eg: onExecute => app.<appname>.open
285
	 *
286
	 * @param _action
287
	 * @param _senders
288
	 */
289
	open(_action, _senders) {
290
		var id_app = _senders[0].id.split('::');
291
		egw.open(id_app[1], this.appname);
292
	}
293
294
	_do_action(action_id : string, selected : [])
295
	{
296
	}
297
298
	/**
299
	 * A generic method to action to server asynchronously
300
	 *
301
	 * Designed to be used with the action system as a callback.
302
	 * In the PHP side, set the action
303
	 * 'onExecute' => 'javaScript:app.<appname>.action', and
304
	 * implement _do_action(action_id, selected)
305
	 *
306
	 * @param {egwAction} _action
307
	 * @param {egwActionObject[]} _elems
308
	 */
309
	action(_action, _elems)
310
	{
311
		// let user confirm select-all
312
		var select_all = _action.getManager().getActionById("select_all");
313
		var confirm_msg = (_elems.length > 1 || select_all && select_all.checked) &&
314
			typeof _action.data.confirm_multiple != 'undefined' ?
315
				_action.data.confirm_multiple : _action.data.confirm;
316
317
		if (typeof confirm_msg != 'undefined')
318
		{
319
			var that = this;
320
			var action_id = _action.id;
321
			et2_dialog.show_dialog(function(button_id,value)
322
			{
323
				if (button_id != et2_dialog.NO_BUTTON)
324
				{
325
					that._do_action(action_id, _elems);
326
				}
327
			}, confirm_msg, egw.lang('Confirmation required'), et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE);
328
		}
329
		else if (typeof this._do_action == 'function')
330
		{
331
			this._do_action(_action.id, _elems);
332
		}
333
		else
334
		{
335
			// If this is a nextmatch action, do an ajax submit setting the action
336
			var nm = null;
337
			var action = _action;
338
			while(nm == null && action.parent != null)
339
			{
340
				if(action.data.nextmatch) nm = action.data.nextmatch;
341
				action = action.parent;
342
			}
343
			if(nm != null)
344
			{
345
				var value = {};
346
				value[nm.options.settings.action_var] = _action.id;
347
				nm.set_value(value);
348
				nm.getInstanceManager().submit();
349
			}
350
		}
351
	}
352
353
	/**
354
	 * Set the application's state to the given state.
355
	 *
356
	 * While not pretending to implement the history API, it is patterned similarly
357
	 * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html
358
	 *
359
	 * The default implementation works with the favorites to apply filters to a nextmatch.
360
	 *
361
	 *
362
	 * @param {{name: string, state: object}|string} state Object (or JSON string) for a state.
363
	 *	Only state is required, and its contents are application specific.
364
	 * @param {string} template template name to check, instead of trying all templates of current app
365
	 * @return {boolean} false - Returns false to stop event propagation
366
	 */
367
	setState(state, template? : string) : string|false|void
368
	{
369
		// State should be an object, not a string, but we'll parse
370
		if(typeof state == "string")
371
		{
372
			if(state.indexOf('{') != -1 || state =='null')
373
			{
374
				state = JSON.parse(state);
375
			}
376
		}
377
		if(typeof state != "object")
378
		{
379
			egw.debug('error', 'Unable to set state to %o, needs to be an object',state);
380
			return;
381
		}
382
		if(state == null)
383
		{
384
			state = {};
385
		}
386
387
		// Check for egw.open() parameters
388
		if(state.state && state.state.id && state.state.app)
389
		{
390
			return egw.open(state.state,undefined,undefined,{},'_self');
391
		}
392
393
		// Try and find a nextmatch widget, and set its filters
394
		var nextmatched = false;
395
		var et2 = template ? etemplate2.getByTemplate(template) : etemplate2.getByApplication(this.appname);
396
		for(var i = 0; i < et2.length; i++)
397
		{
398
			et2[i].widgetContainer.iterateOver(function(_widget) {
399
				// Firefox has trouble with spaces in search
400
				if(state.state && state.state.search) state.state.search = unescape(state.state.search);
401
402
				// Apply
403
				if(state.state && state.state.sort && state.state.sort.id)
404
				{
405
					_widget.sortBy(state.state.sort.id, state.state.sort.asc,false);
406
				}
407
				if(state.state && state.state.selectcols)
408
				{
409
					// Make sure it's a real array, not an object, then set cols
410
					_widget.set_columns(jQuery.extend([],state.state.selectcols));
411
				}
412
				_widget.applyFilters(state.state || state.filter || {});
413
				nextmatched = true;
414
			}, this, et2_nextmatch);
415
			if(nextmatched) return false;
416
		}
417
418
		// 'blank' is the special name for no filters, send that instead of the nice translated name
419
		var safe_name = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state||state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_');
420
		var url = '/'+this.appname+'/index.php';
421
422
		// Try a redirect to list, if app defines a "list" value in registry
423
		if (egw.link_get_registry(this.appname, 'list'))
424
		{
425
			url = egw.link('/index.php', jQuery.extend({'favorite': safe_name}, egw.link_get_registry(this.appname, 'list')));
426
		}
427
		// if no list try index value from application
428
		else if (egw.app(this.appname)?.index)
429
		{
430
			url = egw.link('/index.php', 'menuaction='+egw.app(this.appname).index+'&favorite='+safe_name);
431
		}
432
		egw.open_link(url, undefined, undefined, this.appname);
433
		return false;
434
	}
435
436
	/**
437
	 * Retrieve the current state of the application for future restoration
438
	 *
439
	 * The state can be anything, as long as it's an object.  The contents are
440
	 * application specific.  The default implementation finds a nextmatch and
441
	 * returns its value.
442
	 * The return value of this function cannot be passed directly to setState(),
443
	 * since setState is expecting an additional wrapper, eg:
444
	 * {name: 'something', state: getState()}
445
	 *
446
	 * @return {object} Application specific map representing the current state
447
	 */
448
	getState() : {[propName:string]: any}
449
	{
450
		var state = {};
451
452
		// Try and find a nextmatch widget, and set its filters
453
		var et2 = etemplate2.getByApplication(this.appname);
454
		for(var i = 0; i < et2.length; i++)
455
		{
456
			et2[i].widgetContainer.iterateOver(function(_widget) {
457
				state = _widget.getValue();
458
			}, this, et2_nextmatch);
459
		}
460
461
		return state;
462
	}
463
464
	/**
465
	 * Function to load selected row from nm into a template view
466
	 *
467
	 * @param {object} _action
468
	 * @param {object} _senders
469
	 * @param {boolean} _noEdit defines whether to set edit button or not default is false
470
	 * @param {function} et2_callback function to run after et2 is loaded
471
	 */
472
	viewEntry(_action, _senders, _noEdit, et2_callback)
473
	{
474
		//full id in nm
475
		var id = _senders[0].id;
476
		// flag for edit button
477
		var noEdit = _noEdit || false;
478
		// nm row id
479
		var rowID = '';
480
		// content to feed to etemplate2
481
		var content:any = {};
482
483
		var self = this;
484
485
		if (id){
486
			var parts = id.split('::');
487
			rowID = parts[1];
488
			content = egw.dataGetUIDdata(id);
489
			if (content.data) content = content.data;
490
		}
491
492
		// create a new app object with just constructors for our new etemplate2 object
493
		var app = { classes: window.app.classes };
494
495
		/* destroy generated etemplate for view mode in DOM*/
496
		var destroy = function(){
497
			self.viewContainer.remove();
498
			delete self.viewTemplate;
499
			delete self.viewContainer;
500
			delete self.et2_view;
501
			// we need to reference back into parent context this
502
			for (var v in self)
503
			{
504
				this[v] = self[v];
505
			}
506
			app = null;
507
		};
508
509
		// view container
510
		this.viewContainer = jQuery(document.createElement('div'))
511
				.addClass('et2_mobile_view')
512
				.css({
513
					"z-index":102,
514
					width:"100%",
515
					height:"100%",
516
					background:"white",
517
					display:'block',
518
					position: 'absolute',
519
					left:0,
520
					bottom:0,
521
					right:0,
522
					overflow:'auto'
523
				})
524
				.attr('id','popupMainDiv')
525
				.appendTo('body');
526
527
		// close button
528
		var close = jQuery(document.createElement('span'))
529
				.addClass('egw_fw_mobile_popup_close loaded')
530
				.click(function(){
531
					destroy.call(app[self.appname]);
532
					//disable selected actions after close
533
					egw_globalObjectManager.setAllSelected(false);
534
				})
535
				.appendTo(this.viewContainer);
536
		if (!noEdit)
537
		{
538
			// edit button
539
			var edit = jQuery(document.createElement('span'))
540
					.addClass('mobile-view-editBtn')
541
					.click(function(){
542
						egw.open(rowID, self.appname);
543
					})
544
					.appendTo(this.viewContainer);
545
		}
546
		// view template main container (content)
547
		this.viewTemplate = jQuery(document.createElement('div'))
548
				.attr('id', this.appname+'-view')
549
				.addClass('et2_mobile-view-container popupMainDiv')
550
				.appendTo(this.viewContainer);
551
552
		var mobileViewTemplate = (_action.data.mobileViewTemplate ||'edit').split('?');
553
		var templateName = mobileViewTemplate[0];
554
		var templateTimestamp = mobileViewTemplate[1];
555
		var templateURL = egw.webserverUrl+ '/' + this.appname + '/templates/mobile/'+templateName+'.xet'+'?'+templateTimestamp;
556
557
		var data = {
558
			'content': content,
559
			'readonlys': {'__ALL__':true,'link_to':false},
560
			'currentapp': this.appname,
561
			'langRequire': this.et2.getArrayMgr('langRequire').data,
562
			'sel_options': this.et2.getArrayMgr('sel_options').data,
563
			'modifications': this.et2.getArrayMgr('modifications').data,
564
			'validation_errors': this.et2.getArrayMgr('validation_errors').data
565
		};
566
567
		// etemplate2 object for view
568
		this.et2_view = new etemplate2 (this.viewTemplate[0], false);
569
		framework.pushState('view');
570
		if(templateName)
571
		{
572
			this.et2_view.load(this.appname+'.'+templateName,templateURL, data, typeof et2_callback == 'function'?et2_callback:function(){}, app);
573
		}
574
575
		// define a global close function for view template
576
		// in order to be able to destroy view on action
577
		this.et2_view.close = destroy;
578
	}
579
580
	/**
581
	 * Initializes actions and handlers on sidebox (delete)
582
	 *
583
	 * @param {jQuery} sidebox jQuery of DOM node
584
	 */
585
	_init_sidebox(sidebox)
586
	{
587
		// Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object
588
		if (!this.egw.is_popup())
589
		{
590
			var egw_fw = egw_getFramework();
591
			var tutorial = jQuery('#egw_tutorial_'+this.appname+'_sidebox', egw_fw ? egw_fw.sidemenuDiv : document);
592
			// _init_sidebox gets currently called multiple times, which needs to be fixed
593
			if (tutorial.length && !this.tutorial_initialised)
594
			{
595
				this.egwTutorial_init(tutorial[0]);
596
				this.tutorial_initialised = true;
597
			}
598
		}
599
		if(sidebox.length)
600
		{
601
			var self = this;
602
			if(this.sidebox) this.sidebox.off();
603
			this.sidebox = sidebox;
604
			sidebox
605
				.off()
606
				// removed .on("mouse(enter|leave)" (wrapping trash icon), as it stalls delete in IE11
607
				.on("click.sidebox","div.ui-icon-trash", this, this.delete_favorite)
608
				// need to install a favorite handler, as we switch original one off with .off()
609
				.on('click.sidebox','li[data-id]', this, function(event) {
610
					var li = jQuery(this);
611
					li.siblings().removeClass('ui-state-highlight');
612
613
					var state = {};
614
					var pref = egw.preference('favorite_' + this.dataset.id, self.appname);
615
					if(pref)
616
					{
617
						// Extend, to prevent changing the preference by reference
618
						jQuery.extend(true, state, pref);
619
					}
620
					if(this.dataset.id != 'add')
621
					{
622
						event.stopImmediatePropagation();
623
						self.setState.call(self, state);
624
						return false;
625
					}
626
				})
627
				.addClass("ui-helper-clearfix");
628
629
			//Add Sortable handler to sideBox fav. menu
630
			jQuery('ul','#favorite_sidebox_'+this.appname).sortable({
631
					items:'li:not([data-id$="add"])',
632
					placeholder:'ui-fav-sortable-placeholder',
633
					delay:250, //(millisecond) delay before the sorting should start
634
					helper: function(event, item : any) {
635
						// We'll need to know which app this is for
636
						item.attr('data-appname',self.appname);
637
						// Create custom helper so it can be dragged to Home
638
						var h_parent = item.parent().parent().clone();
639
						h_parent.find('li').not('[data-id="'+item.attr('data-id')+'"]').remove();
640
						h_parent.appendTo('body');
641
						return h_parent;
642
					},
643
					// @ts-ignore
644
					refreshPositions: true,
645
					update: function (event, ui)
646
					{
647
						// @ts-ignore
648
						var favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'});
649
650
						self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList);
651
652
						self._refresh_fav_nm();
653
					}
654
				});
655
656
			// Bind favorite de-select
657
			var egw_fw = egw_getFramework();
658
			if(egw_fw && egw_fw.applications[this.appname] && egw_fw.applications[this.appname].browser
659
				&& egw_fw.applications[this.appname].browser.baseDiv)
660
			{
661
				jQuery(egw_fw.applications[this.appname].browser.baseDiv)
662
					.off('.sidebox')
663
					.on('change.sidebox', function() {
664
						self.highlight_favorite();
665
					});
666
			}
667
			return true;
668
		}
669
		return false;
670
	}
671
672
	/**
673
	 * Add a new favorite
674
	 *
675
	 * Fetches the current state from the application, then opens a dialog to get the
676
	 * name and other settings.  If user proceeds, the favorite is saved, and if possible
677
	 * the sidebox is directly updated to include the new favorite
678
	 *
679
	 * @param {object} [state] State settings to be merged into the application state
680
	 */
681
	add_favorite(state)
682
	{
683
		if(typeof this.favorite_popup == "undefined" || // Create popup if it's not defined yet
684
			(this.favorite_popup && typeof this.favorite_popup.group != "undefined"
685
			&& !this.favorite_popup.group.isAttached())) // recreate the favorite popup if the group selectbox is not attached (eg. after et2 submit)
686
		{
687
			this._create_favorite_popup();
688
		}
689
		// Get current state
690
		this.favorite_popup.state = jQuery.extend({}, this.getState(), state||{});
691
/*
692
		// Add in extras
693
		for(var extra in this.options.filters)
694
		{
695
			// Don't overwrite what nm has, chances are nm has more up-to-date value
696
			if(typeof this.popup.current_filters == 'undefined')
697
			{
698
				this.popup.current_filters[extra] = this.nextmatch.options.settings[extra];
699
			}
700
		}
701
702
		// Add in application's settings
703
		if(this.filters != true)
704
		{
705
			for(var i = 0; i < this.filters.length; i++)
706
			{
707
				this.popup.current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]];
708
			}
709
		}
710
*/
711
		// Make sure it's an object - deep copy to prevent references in sub-objects (col_filters)
712
		this.favorite_popup.state = jQuery.extend(true,{},this.favorite_popup.state);
713
714
		// Update popup with current set filters (more for debug than user)
715
		var filter_list = [];
716
		var add_to_popup = function(arr) {
717
			filter_list.push("<ul>");
718
			jQuery.each(arr, function(index, filter) {
719
				filter_list.push("<li id='index'><span class='filter_id'>"+index+"</span>" +
720
					(typeof filter != "object" ? "<span class='filter_value'>"+filter+"</span>": "")
721
				);
722
				if(typeof filter == "object" && filter != null) add_to_popup(filter);
723
				filter_list.push("</li>");
724
			});
725
			filter_list.push("</ul>");
726
		};
727
		add_to_popup(this.favorite_popup.state);
728
		jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup)
729
			.replaceWith(
730
				jQuery(filter_list.join("")).attr("id",this.appname+"_favorites_popup_state")
731
			);
732
		jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup)
733
			.hide()
734
			.siblings(".ui-icon-circle-plus")
735
			.removeClass("ui-icon-circle-minus");
736
737
		// Popup
738
		this.favorite_popup.dialog("open");
739
		console.log(this);
740
741
		// Stop the normal bubbling if this is called on click
742
		return false;
743
	}
744
745
	/**
746
	 * Update favorite items in nm fav. menu
747
	 *
748
	 */
749
	_refresh_fav_nm ()
750
	{
751
		var self = this;
752
753
		if(etemplate2 && etemplate2.getByApplication)
754
		{
755
			var et2 = etemplate2.getByApplication(self.appname);
756
			for(var i = 0; i < et2.length; i++)
757
			{
758
				et2[i].widgetContainer.iterateOver(function(_widget) {
759
					_widget.stored_filters = _widget.load_favorites(self.appname);
760
					_widget.init_filters(_widget);
761
				}, self, et2_favorites);
762
			}
763
		}
764
		else
765
		{
766
			throw new Error ("_refresh_fav_nm():Either et2 is  not ready/ not there yet. Make sure that etemplate2 is ready before call this method.");
767
		}
768
	}
769
770
	/**
771
	 * Create the "Add new" popup dialog
772
	 */
773
	_create_favorite_popup()
774
	{
775
		var self = this;
776
		var favorite_prefix = 'favorite_';
777
778
		// Clear old, if existing
779
		if(this.favorite_popup && this.favorite_popup.group)
780
		{
781
			this.favorite_popup.group.free();
782
			delete this.favorite_popup;
783
		}
784
785
		// Create popup
786
		this.favorite_popup = jQuery('<div id="'+this.dom_id + '_nm_favorites_popup" title="' + egw().lang("New favorite") + '">\
787
			<form>\
788
			<label for="name">'+
789
				this.egw.lang("name") +
790
			'</label>' +
791
792
			'<input type="text" name="name" id="name"/>\
793
			<div id="'+this.appname+'_favorites_popup_admin"/>\
794
			<span>'+ this.egw.lang("Details") + '</span><span style="float:left;" class="ui-icon ui-icon-circle-plus ui-button" />\
795
			<ul id="'+this.appname+'_favorites_popup_state"/>\
796
			</form>\
797
			</div>'
798
		).appendTo(this.et2 ? this.et2.getDOMNode() : jQuery('body'));
799
800
		// @ts-ignore
801
		jQuery(".ui-icon-circle-plus",this.favorite_popup).prev().andSelf().click(function() {
802
			var details = jQuery("#"+self.appname+"_favorites_popup_state",self.favorite_popup)
803
				.slideToggle()
804
				.siblings(".ui-icon-circle-plus")
805
				.toggleClass("ui-icon-circle-minus");
806
		});
807
808
		// Add some controls if user is an admin
809
		var apps = egw().user('apps');
810
		var is_admin = (typeof apps['admin'] != "undefined");
811
		if(is_admin)
812
		{
813
			this.favorite_popup.group = et2_createWidget("select-account",{
814
				id: "favorite[group]",
815
				account_type: "groups",
816
				empty_label: "Groups",
817
				no_lang: true,
818
				parent_node: this.appname+'_favorites_popup_admin'
819
			},(this.et2 || null));
820
			this.favorite_popup.group.loadingFinished();
821
		}
822
823
		var buttons = {};
824
		buttons['save'] = {
825
			text: this.egw.lang('save'),
826
			default: true,
827
			style: 'background-image: url('+this.egw.image('save')+')',
828
			click: function() {
829
				// Add a new favorite
830
				var name = jQuery("#name",this);
831
832
				if(name.val())
833
				{
834
					// Add to the list
835
					name.val(name.val().replace(/(<([^>]+)>)/ig,""));
836
					var safe_name = name.val().replace(/[^A-Za-z0-9-_]/g,"_");
837
					var favorite = {
838
						name: name.val(),
839
						group: (typeof self.favorite_popup.group != "undefined" &&
840
							self.favorite_popup.group.get_value() ? self.favorite_popup.group.get_value() : false),
841
						state: self.favorite_popup.state
842
					};
843
844
					var favorite_pref = favorite_prefix+safe_name;
845
846
					// Save to preferences
847
					if(typeof self.favorite_popup.group != "undefined" && self.favorite_popup.group.getValue() != '')
848
					{
849
						// Admin stuff - save preference server side
850
						self.egw.jsonq('EGroupware\\Api\\Framework::ajax_set_favorite',
851
							[
852
								self.appname,
853
								name.val(),
854
								"add",
855
								self.favorite_popup.group.get_value(),
856
								self.favorite_popup.state
857
							]
858
						);
859
						self.favorite_popup.group.set_value('');
860
					}
861
					else
862
					{
863
						// Normal user - just save to preferences client side
864
						self.egw.set_preference(self.appname,favorite_pref,favorite);
865
					}
866
867
					// Add to list immediately
868
					if(self.sidebox)
869
					{
870
						// Remove any existing with that name
871
						jQuery('[data-id="'+safe_name+'"]',self.sidebox).remove();
872
873
						// Create new item
874
						var html = "<li data-id='"+safe_name+"' data-group='" + favorite.group + "' class='ui-menu-item' role='menuitem'>\n";
875
						var href = 'javascript:app.'+self.appname+'.setState('+JSON.stringify(favorite)+');';
876
						html += "<a href='"+href+"' class='ui-corner-all' tabindex='-1'>";
877
						html += "<div class='" + 'sideboxstar' + "'></div>"+
878
							favorite.name;
879
						html += "<div class='ui-icon ui-icon-trash' title='" + egw.lang('Delete') + "'/>";
880
						html += "</a></li>\n";
881
						jQuery(html).insertBefore(jQuery('li',self.sidebox).last());
882
						self._init_sidebox(self.sidebox);
883
					}
884
885
					// Try to update nextmatch favorites too
886
					self._refresh_fav_nm();
887
				}
888
				// Reset form
889
				delete self.favorite_popup.state;
890
				name.val("");
891
				jQuery("#filters",self.favorite_popup).empty();
892
893
				jQuery(this).dialog("close");
894
			}
895
		};
896
		buttons['cancel'] = {
897
			text: this.egw.lang("cancel"),
898
			style: 'background-image: url('+this.egw.image('cancel')+')',
899
			click: function() {
900
				if(typeof self.favorite_popup.group !== 'undefined' && self.favorite_popup.group.set_value)
901
				{
902
					self.favorite_popup.group.set_value(null);
903
				}
904
				jQuery(this).dialog("close");
905
			}
906
		};
907
908
		this.favorite_popup.dialog({
909
			autoOpen: false,
910
			modal: true,
911
			buttons: buttons,
912
			close: function() {
913
			}
914
		});
915
916
		// Bind handler for enter keypress
917
		this.favorite_popup.off('keydown').on('keydown', jQuery.proxy(function(e) {
918
			 var tagName = e.target.tagName.toLowerCase();
919
			tagName = (tagName === 'input' && e.target.type === 'button') ? 'button' : tagName;
920
921
			if(e.keyCode == jQuery.ui.keyCode.ENTER && tagName !== 'textarea' && tagName !== 'select' && tagName !=='button')
922
			{
923
				e.preventDefault();
924
				jQuery('button[default]',this.favorite_popup.parent()).trigger('click');
925
				return false;
926
			}
927
		},this));
928
929
		return false;
930
	}
931
932
	/**
933
	 * Delete a favorite from the list and update preferences
934
	 * Registered as a handler on the delete icons
935
	 *
936
	 * @param {jQuery.event} event event object
937
	 */
938
	delete_favorite(event)
939
	{
940
		// Don't do the menu
941
		event.stopImmediatePropagation();
942
943
		var app = event.data;
944
		var id = jQuery(this).parentsUntil('li').parent().attr("data-id");
945
		var group = jQuery(this).parentsUntil('li').parent().attr("data-group") || '';
946
		var line = jQuery('li[data-id="'+id+'"]',app.sidebox);
947
		var name = line.first().text();
948
		var trash = this;
949
		line.addClass('loading');
950
951
		// Make sure first
952
		var do_delete = function(button_id)
953
		{
954
			if(button_id != et2_dialog.YES_BUTTON)
955
			{
956
				line.removeClass('loading');
957
				return;
958
			}
959
960
			// Hide the trash
961
			jQuery(trash).hide();
962
963
			// Delete preference server side
964
			var request = egw.json("EGroupware\\Api\\Framework::ajax_set_favorite",
965
				[app.appname, id, "delete", group, ''],
966
				function(result) {
967
					// Got the full response from callback, which we don't want
968
					if(result.type) return;
969
970
					if(result && typeof result == 'boolean')
971
					{
972
						// Remove line from list
973
						line.slideUp("slow", function() { });
974
975
						app._refresh_fav_nm();
976
					}
977
					else
978
					{
979
						// Something went wrong server side
980
						line.removeClass('loading').addClass('ui-state-error');
981
					}
982
				},
983
				jQuery(trash).parentsUntil("li").parent(),
984
				true,
985
				jQuery(trash).parentsUntil("li").parent()
986
			);
987
			request.sendRequest(true);
988
		};
989
		et2_dialog.show_dialog(do_delete, (egw.lang("Delete") + " " +name +"?"),
990
			egw.lang("Delete"), et2_dialog.YES_NO, et2_dialog.QUESTION_MESSAGE);
991
992
		return false;
993
	}
994
995
	/**
996
	 * Mark the favorite closest matching the current state
997
	 *
998
	 * Closest matching takes into account not set values, so we pick the favorite
999
	 * with the most matching values without a value that differs.
1000
	 */
1001
	highlight_favorite() {
1002
		if(!this.sidebox) return;
1003
1004
		var state = this.getState();
1005
		var best_match = false;
1006
		var best_count = 0;
1007
		var self = this;
1008
1009
		jQuery('li[data-id]',this.sidebox).removeClass('ui-state-highlight');
1010
1011
		jQuery('li[data-id]',this.sidebox).each(function(i,href) {
1012
			var favorite : any = {};
1013
			if(this.dataset.id && egw.preference('favorite_'+this.dataset.id,self.appname))
1014
			{
1015
				favorite = egw.preference('favorite_'+this.dataset.id,self.appname);
1016
			}
1017
			if(!favorite || jQuery.isEmptyObject(favorite)) return;
1018
1019
			// Handle old style by making it like new style
1020
			if(favorite.filter && !favorite.state)
1021
			{
1022
				favorite.state = favorite.filter;
1023
			}
1024
1025
			var match_count = 0;
1026
			var extra_keys = Object.keys(favorite.state);
1027
			for(var state_key in state)
1028
			{
1029
				extra_keys.splice(extra_keys.indexOf(state_key),1);
1030
				if(typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined'&&typeof favorite.state[state_key] != 'undefined' && ( state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key]))
1031
				{
1032
					match_count++;
1033
				}
1034
				else if (state_key == 'selectcols')
1035
				{
1036
					// Skip, might be set, might not
1037
				}
1038
				else if (typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object'
1039
							&& typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object')
1040
				{
1041
					if((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined')
1042
							&& (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length ))
1043
					{
1044
						// State or favorite has a length, but the other does not
1045
						if((state[state_key].length === 0 || Object.keys(state[state_key]).length === 0) &&
1046
							(favorite.state[state_key].length == 0 || Object.keys(favorite.state[state_key]).length === 0))
1047
						{
1048
							// Just missing, or one is an array and the other is an object
1049
							continue;
1050
						}
1051
						// One has a value and the other doesn't, no match
1052
						return;
1053
					}
1054
					else if (state[state_key].length !== 'undefined' && typeof favorite.state[state_key].length !== 'undefined' &&
1055
						state[state_key].length === 0 && favorite.state[state_key].length === 0)
1056
					{
1057
						// Both set, but both empty
1058
						match_count++;
1059
						continue;
1060
					}
1061
					// Consider sub-objects (column filters) individually
1062
					for(var sub_key in state[state_key])
1063
					{
1064
						if(state[state_key][sub_key] == favorite.state[state_key][sub_key] || !state[state_key][sub_key] && !favorite.state[state_key][sub_key])
1065
						{
1066
							match_count++;
1067
						}
1068
						else if (state[state_key][sub_key] && favorite.state[state_key][sub_key] &&
1069
							typeof state[state_key][sub_key] === 'object' && typeof favorite.state[state_key][sub_key] === 'object')
1070
						{
1071
							// Too deep to keep going, just string compare for perfect match
1072
							if(JSON.stringify(state[state_key][sub_key]) === JSON.stringify(favorite.state[state_key][sub_key]))
1073
							{
1074
								match_count++;
1075
							}
1076
						}
1077
						else if(typeof state[state_key][sub_key] !== 'undefined' && state[state_key][sub_key] != favorite.state[state_key][sub_key])
1078
						{
1079
							// Different values, do not match
1080
							return;
1081
						}
1082
					}
1083
				}
1084
				else if (typeof state[state_key] !== 'undefined'
1085
						 && typeof favorite.state != 'undefined'&&typeof favorite.state[state_key] !== 'undefined'
1086
						 && state[state_key] != favorite.state[state_key])
1087
				{
1088
					// Different values, do not match
1089
					return;
1090
				}
1091
			}
1092
			// Check for anything set that the current one does not have
1093
			for(var i = 0; i < extra_keys.length; i++)
1094
			{
1095
				if(favorite.state[extra_keys[i]]) return;
1096
			}
1097
			if(match_count > best_count)
1098
			{
1099
				best_match = this.dataset.id;
1100
				best_count = match_count;
1101
			}
1102
		});
1103
		if(best_match)
1104
		{
1105
			jQuery('li[data-id="'+best_match+'"]',this.sidebox).addClass('ui-state-highlight');
1106
		}
1107
	}
1108
1109
	/**
1110
	 * Fix scrolling iframe browsed by iPhone/iPod/iPad touch devices
1111
	 */
1112
	_fix_iFrameScrolling()
1113
	{
1114
		if (/iPhone|iPod|iPad/.test(navigator.userAgent))
1115
		{
1116
			jQuery("iframe").on({
1117
				load: function()
1118
				{
1119
					var body = this.contentWindow.document.body;
1120
1121
					var div = jQuery(document.createElement("div"))
1122
							.css ({
1123
								'height' : jQuery(this.parentNode).height(),
1124
								'width' : jQuery(this.parentNode).width(),
1125
								'overflow' : 'scroll'});
1126
					while (body.firstChild)
1127
					{
1128
						div.append(body.firstChild);
1129
					}
1130
					jQuery(body).append(div);
1131
				}
1132
			});
1133
		}
1134
	}
1135
1136
	/**
1137
	 * Set document title, uses getWindowTitle to get the correct title,
1138
	 * otherwise set it with uniqueID as default title
1139
	 */
1140
	_set_Window_title ()
1141
	{
1142
		var title = this.getWindowTitle();
1143
		if (title)
1144
		{
1145
			document.title = this.et2._inst.uniqueId + ": " + title;
1146
		}
1147
	}
1148
1149
	/**
1150
	 * Window title getter function in order to set the window title
1151
	 * this can be overridden on each application app.js file to customize the title value
1152
	 *
1153
	 * @returns {string} window title
1154
	 */
1155
	getWindowTitle ()
1156
	{
1157
		var titleWidget = this.et2.getWidgetById('title');
1158
		if (titleWidget)
1159
		{
1160
			return titleWidget.options.value;
1161
		}
1162
		else
1163
		{
1164
			return this.et2._inst.uniqueId;
1165
		}
1166
	}
1167
1168
	/**
1169
	 * Handler for drag and drop when dragging nextmatch rows from mail app
1170
	 * and dropped on a row in the current application.  We copy the mail into
1171
	 * the filemanager to link it since we can't link directly.
1172
	 *
1173
	 * This doesn't happen automatically.  Each application must indicate that
1174
	 * it will accept dropped mail by it's nextmatch actions:
1175
	 *
1176
	 * $actions['info_drop_mail'] = array(
1177
	 *		'type' => 'drop',
1178
	 *		'acceptedTypes' => 'mail',
1179
	 *		'onExecute' => 'javaScript:app.infolog.handle_dropped_mail',
1180
	 *		'hideOnDisabled' => true
1181
	 *	);
1182
	 *
1183
	 * This action, when defined, will not affect the automatic linking between
1184
	 * normal applications.
1185
	 *
1186
	 * @param {egwAction} _action
1187
	 * @param {egwActionObject[]} _selected Dragged mail rows
1188
	 * @param {egwActionObject} _target Current application's nextmatch row the mail was dropped on
1189
	 */
1190
	handle_dropped_mail(_action, _selected, _target)
1191
	{
1192
		/**
1193
		 * Mail doesn't support link system, so we copy it to VFS
1194
		 */
1195
		var ids = _target.id.split("::");
1196
		if(ids.length != 2 || ids[0] == 'mail') return;
1197
1198
		var vfs_path = "/apps/"+ids[0]+"/"+ids[1];
1199
		var mail_ids = [];
1200
1201
		for(var i = 0; i < _selected.length; i++)
1202
		{
1203
			mail_ids.push(_selected[i].id);
1204
		}
1205
		if(mail_ids.length)
1206
		{
1207
			egw.message(egw.lang("Please wait..."));
1208
			this.egw.json('filemanager.filemanager_ui.ajax_action',['mail',mail_ids, vfs_path],function(data){
1209
				// Trigger an update (minimal, no sorting changes) to display the new link
1210
				egw.refresh(data.msg||'',ids[0],ids[1],'update');
1211
			}).sendRequest(true);
1212
		}
1213
	}
1214
1215
	/**
1216
	 * Get json data for videos from the given url
1217
	 *
1218
	 * @return {Promise, object} return Promise, json object as resolved result and error message in case of failure
1219
	 */
1220
	egwTutorialGetData(){
1221
		var self = this;
1222
		return new Promise (function(_resolve, _reject)
1223
		{
1224
			var resolve = _resolve;
1225
			var reject = _reject;
1226
			// delay the execution and let the rendering catches up. Seems only FF problem
1227
			window.setTimeout(function(){
1228
				self.egw.json('EGroupware\\Api\\Framework\\Tutorial::ajax_data', [self.egw.app_name()], function(_data){
1229
					resolve(_data);
1230
				}).sendRequest();
1231
			},0);
1232
1233
		});
1234
	}
1235
1236
	/**
1237
	 * Create and Render etemplate2 for egroupware tutorial
1238
	 * sidebox option. The .xet file is stored in api/templates/default/egw_tutorials
1239
	 *
1240
	 * @description tutorials json object should have the following structure:
1241
	 *	object:
1242
	 *		{
1243
	 *			[app name]:{
1244
	 *				[language tag]:[
1245
	 *					{src:"",thumbnail:"",title:"",desc:""}
1246
	 *				]
1247
	 *			}
1248
	 *		}
1249
	 *
1250
	 *	*Note: "desc" and "title" are optional attributes, which "desc" would appears as tooltip for the video.
1251
	 *
1252
	 *	example:
1253
	 *		{
1254
	 *			"mail":{
1255
	 *				"en":[
1256
	 *					{src:"https://www.youtube.com/embed/mCDJndpjO40", thumbnail:"http://img.youtube.com/vi/mCDJndpjO40/0.jpg", "title":"PGP Encryption", "desc":""},
1257
	 *					{src:"https://www.youtube.com/embed/mCDJndpjO", thumbnail:"http://img.youtube.com/vi/mCDJndpjO/0.jpg", "title":"Subscription", "desc":""},
1258
	 *				],
1259
	 *				"de":[
1260
	 *					{src:"https://www.youtube.com/embed/m40", thumbnail:"http://img.youtube.com/vi/m40/0.jpg", "title":"PGP Verschlüsselung", "desc":""},
1261
	 *					{src:"https://www.youtube.com/embed/mpjO", thumbnail:"http://img.youtube.com/vi/mpjO/0.jpg", "title":"Ordner Abonnieren", "desc":""},
1262
	 *				]
1263
	 *			}
1264
	 *		}
1265
	 *
1266
	 * @param {DOMNode} div
1267
	 */
1268
	egwTutorial_init(div)
1269
	{
1270
		// et2 object
1271
		var etemplate = new etemplate2 (div, false);
1272
		var template = egw.webserverUrl+'/api/templates/default/egw_tutorial.xet?1';
1273
1274
		this.egwTutorialGetData().then(function(_data){
1275
			var lang = egw.preference('lang');
1276
			var content = {content:{list:[]}};
1277
			if (_data && _data[egw.app_name()])
1278
			{
1279
				if (!_data[egw.app_name()][lang]) lang = 'en';
1280
				if (typeof _data[egw.app_name()][lang] !='undefined'
1281
					&& _data[egw.app_name()][lang].length > 0)
1282
				{
1283
					for (var i=0;i < _data[egw.app_name()][lang].length;i++)
1284
					{
1285
						var tuid = egw.app_name() + '-' +lang + '-' + i;
1286
						_data[egw.app_name()][lang][i]['onclick'] = 'app.'+egw.app_name()+'.egwTutorialPopup("'+tuid+'")';
1287
					}
1288
					content.content.list = _data[egw.app_name()][lang];
1289
1290
					if (template.indexOf('.xet') >0)
1291
					{
1292
						etemplate.load ('',template , content, function(){});
1293
					}
1294
					else
1295
					{
1296
						etemplate.load (template, '', content);
1297
					}
1298
				}
1299
			}
1300
		},
1301
		function(_err){
1302
			console.log(_err);
1303
		});
1304
	}
1305
1306
	/**
1307
	 * Open popup to show given tutorial id
1308
	 * @param {string} _tuid tutorial object id
1309
	 *	- tuid: appname-lang-index
1310
	 */
1311
	egwTutorialPopup (_tuid)
1312
	{
1313
		var url = egw.link('/index.php', 'menuaction=api.EGroupware\\Api\\Framework\\Tutorial.popup&tuid='+_tuid);
1314
		egw.open_link(url,'_blank','960x580');
1315
	}
1316
1317
	/**
1318
	 * Function to set video iframe base on selected tutorial from tutorials box
1319
	 *
1320
	 * @param {string} _url
1321
	 */
1322
	tutorial_videoOnClick (_url)
1323
	{
1324
		var frame = etemplate2.getByApplication('api')[0].widgetContainer.getWidgetById('src');
1325
		if (frame)
1326
		{
1327
			frame.set_value(_url);
1328
		}
1329
	}
1330
1331
	/**
1332
	 * Function calls on discard checkbox and will set
1333
	 * the egw_tutorial_noautoload preference
1334
	 *
1335
	 * @param {type} egw
1336
	 * @param {type} widget
1337
	 */
1338
	tutorial_autoloadDiscard (egw, widget)
1339
	{
1340
		if (widget)
1341
		{
1342
			this.egw.set_preference('common', 'egw_tutorial_noautoload', widget.get_value());
1343
		}
1344
	}
1345
1346
	/**
1347
	 * Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it
1348
	 *
1349
	 * @param {function} _callback called if and only if mailvelope is available (context is this!)
1350
	 */
1351
	mailvelopeAvailable(_callback)
1352
	{
1353
		var self = this;
1354
		var callback = jQuery.proxy(_callback, this);
1355
1356
		if (typeof mailvelope !== 'undefined')
1357
		{
1358
			this.mailvelopeOpenKeyring().then(callback);
1359
		}
1360
		else
1361
		{
1362
			jQuery(window).on('mailvelope', function()
1363
			{
1364
				self.mailvelopeOpenKeyring().then(callback);
1365
			});
1366
		}
1367
	}
1368
1369
	/**
1370
	 * mailvelope object contains SyncHandlers
1371
	 *
1372
	 * @property {function} descriptionuploadSync function called by Mailvelope to upload encrypted private key backup
1373
	 * @property {function} downloadSync function called by Mailvelope to download encrypted private key backup
1374
	 * @property {function} backup function called by Mailvelope to upload a public keyring backup
1375
	 * @property {function} restore function called by Mailvelope to restore a public keyring backup
1376
	 */
1377
	private mailvelopeSyncHandler()
1378
	{
1379
		return {
1380
			/**
1381
			 * function called by Mailvelope to upload a public keyring
1382
			 * @param {UploadSyncHandler} _uploadObj
1383
			 *	@property {string} etag entity tag for the uploaded encrypted keyring, or null if initial upload
1384
			 *	@property {AsciiArmored} keyringMsg encrypted keyring as PGP armored message
1385
			 * @returns {Promise.<UploadSyncReply, Error>}
1386
			 */
1387
			uploadSync: function(_uploadObj)
1388
			{
1389
				return new Promise(function(_resolve,_reject){});
1390
			},
1391
1392
			/**
1393
			 * function called by Mailvelope to download a public keyring
1394
			 *
1395
			 * @param {object} _downloadObj
1396
			 *	@property {string} etag entity tag for the current local keyring, or null if no local eTag
1397
			 * @returns {Promise.<DownloadSyncReply, Error>}
1398
			 */
1399
			downloadSync: function(_downloadObj)
1400
			{
1401
				return new Promise(function(_resolve,_reject){});
1402
			},
1403
1404
			/**
1405
			 * function called by Mailvelope to upload an encrypted private key backup
1406
			 *
1407
			 * @param {BackupSyncPacket} _backup
1408
			 *	@property {AsciiArmored} backup an encrypted private key as PGP armored message
1409
			 * @returns {Promise.<undefined, Error>}
1410
			 */
1411
			backup: function(_backup)
1412
			{
1413
				return new Promise(function(_resolve,_reject){
1414
					// Store backup sync packet into .PGP-Key-Backup file in user directory
1415
					jQuery.ajax({
1416
						method:'PUT',
1417
						url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup',
1418
						contentType: 'application/json',
1419
						data: JSON.stringify(_backup),
1420
						success:function(){
1421
							_resolve(_backup);
1422
						},
1423
						error: function(_err){
1424
							_reject(_err);
1425
						}
1426
					});
1427
				});
1428
			},
1429
1430
			/**
1431
			 * function called by Mailvelope to restore an encrypted private key backup
1432
			 *
1433
			 * @returns {Promise.<BackupSyncPacket, Error>}
1434
			 * @todo
1435
			 */
1436
			restore: function()
1437
			{
1438
				return new Promise(function(_resolve,_reject){
1439
					var resolve = _resolve;
1440
					var reject = _reject;
1441
					jQuery.ajax({
1442
						url:egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup',
1443
						method: 'GET',
1444
						success: function(_backup){
1445
							resolve(JSON.parse(_backup));
1446
							egw.message('Your key has been restored successfully.');
1447
						},
1448
						error: function(_err){
1449
							//Try with old back file name
1450
							if (_err.status == 404)
1451
							{
1452
								jQuery.ajax({
1453
									method:'GET',
1454
									url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PK_PGP',
1455
									success: function(_backup){
1456
										resolve(JSON.parse(_backup));
1457
										egw.message('Your key has been restored successfully.');
1458
									},
1459
									error: function(_err){
1460
										_reject(_err);
1461
									}
1462
								});
1463
							}
1464
							else
1465
							{
1466
								_reject(_err);
1467
							}
1468
						}
1469
					});
1470
				});
1471
			}
1472
		};
1473
	}
1474
1475
	/**
1476
	 * Function for backup file operations
1477
	 *
1478
	 * @param {type} _url Url of the backup file
1479
	 * @param {type} _cmd command to operate
1480
	 *	- PUT: to store backup file
1481
	 *	- GET: to read backup file
1482
	 *	- DELETE: to delete backup file
1483
	 *
1484
	 * @param {type} _successCallback function called when the operation is successful
1485
	 * @param {type} _errorCallback function called when the operation fails
1486
	 * @param {type} _data data which needs to be stored in file via PUT command
1487
	 */
1488
	_mailvelopeBackupFileOperator(_url, _cmd, _successCallback, _errorCallback, _data?)
1489
	{
1490
		var ajaxObj = {
1491
			url: _url || egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup',
1492
			method: _cmd,
1493
			success: _successCallback,
1494
			error: _errorCallback
1495
		};
1496
		switch (_cmd)
1497
		{
1498
			case 'PUT':
1499
				jQuery.extend({},ajaxObj, {
1500
					data: JSON.stringify(_data),
1501
					contentType: 'application/json'
1502
				});
1503
				break;
1504
			case 'GET':
1505
				jQuery.extend({},ajaxObj, {
1506
					dataType: 'json'
1507
				});
1508
				break;
1509
			case 'DELETE':
1510
				break;
1511
		}
1512
		jQuery.ajax(ajaxObj);
1513
	}
1514
1515
	/**
1516
	 * Create backup dialog
1517
	 * @param {string} _selector DOM selector to attach backupDialog
1518
	 * @param {boolean} _initSetup determine wheter it's an initialization backup or restore backup
1519
	 *
1520
	 * @returns {Promise.<backupPopupId, Error>}
1521
	 */
1522
	mailvelopeCreateBackupDialog(_selector?, _initSetup?)
1523
	{
1524
		var self = this;
1525
		var selector = _selector || 'body';
1526
		var initSetup = _initSetup;
1527
		jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove();
1528
		return new Promise(function(_resolve, _reject)
1529
		{
1530
			var resolve = _resolve;
1531
			var reject = _reject;
1532
1533
			mailvelope.getKeyring('egroupware').then(function(_keyring : any)
1534
			{
1535
				_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj);
1536
1537
				var options = {
1538
					initialSetup:initSetup
1539
				};
1540
				_keyring.createKeyBackupContainer(selector, options).then(function(_popupId){
1541
					var $backup_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]');
1542
					$backup_selector.css({position:'absolute', "z-index":1});
1543
					_popupId.isReady().then(function(result){
1544
						egw.message('Your key has been backedup into  .PGP-Key-Backup successfully.');
1545
						jQuery(selector).empty();
1546
					});
1547
					resolve(_popupId);
1548
				},
1549
				function(_err){
1550
					reject(_err);
1551
				});
1552
			},
1553
			function(_err)
1554
			{
1555
				reject(_err);
1556
			});
1557
		});
1558
	}
1559
1560
	/**
1561
	 * Delete backup key from filesystem
1562
	 */
1563
	mailvelopeDeleteBackup()
1564
	{
1565
		var self = this;
1566
		et2_dialog.show_dialog(function (_button_id)
1567
		{
1568
			if (_button_id == et2_dialog.YES_BUTTON )
1569
			{
1570
				self._mailvelopeBackupFileOperator(undefined, 'DELETE', function(){
1571
					self.egw.message(self.egw.lang('The backup key has been deleted.'));
1572
				}, function(_err){
1573
					self.egw.message(self.egw.lang('Was not able to delete the backup key because %1',_err));
1574
				});
1575
			}
1576
		},
1577
		self.egw.lang('Are you sure, you would like to delete the backup key?'),
1578
		self.egw.lang('Delete backup key'),
1579
		{}, et2_dialog.BUTTONS_YES_CANCEL, et2_dialog.QUESTION_MESSAGE, undefined, self.egw);
1580
	}
1581
1582
	/**
1583
	 * Create mailvelope restore dialog
1584
	 * @param {string} _selector DOM selector to attach restorDialog
1585
	 * @param {boolean} _restorePassword if true, will restore key password too
1586
	 *
1587
	 * @returns {Promise}
1588
	 */
1589
	mailvelopeCreateRestoreDialog(_selector, _restorePassword)
1590
	{
1591
		var self = this;
1592
		var restorePassword = _restorePassword;
1593
		var selector = _selector || 'body';
1594
		//Clear the
1595
		jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove();
1596
		return new Promise(function(_resolve, _reject){
1597
			var resolve = _resolve;
1598
			var reject = _reject;
1599
1600
			mailvelope.getKeyring('egroupware').then(function(_keyring)
1601
			{
1602
				_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj);
1603
1604
				var options = {
1605
					restorePassword:restorePassword
1606
				};
1607
				_keyring.restoreBackupContainer(selector, options).then(function(_restoreId){
1608
					var $restore_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]');
1609
					$restore_selector.css({position:'absolute', "z-index":1});
1610
					resolve(_restoreId);
1611
				},
1612
				function(_err){
1613
					reject(_err);
1614
				});
1615
			},
1616
			function(_err)
1617
			{
1618
				reject(_err);
1619
			});
1620
		});
1621
	}
1622
1623
	/**
1624
	 * Create a dialog to show all backup/restore options
1625
	 *
1626
	 * @returns {undefined}
1627
	 */
1628
	mailvelopeCreateBackupRestoreDialog()
1629
	{
1630
		var self = this;
1631
		var appname = egw.app_name();
1632
		var menu = [
1633
			// Header row should be empty item 0
1634
			{},
1635
			// Restore Keyring item 1
1636
			{label:"Restore key" ,image:"lock", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo')"},
1637
			// Restore pass phrase item 2
1638
			{label:"Restore password",image:"password", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo', true)"},
1639
			// Delete backup Key item 3
1640
			{label:"Delete backup", image:"delete", onclick:"app."+appname+".mailvelopeDeleteBackup"},
1641
			// Backup Key item 4
1642
			{label:"Backup Key", image:"save", onclick:"app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', false)"}
1643
		];
1644
1645
		var dialog = function(_content, _callback?)
1646
		{
1647
			return et2_createWidget("dialog", {
1648
						callback: function(_button_id, _value) {
1649
							if (typeof _callback == "function")
1650
							{
1651
								_callback.call(this, _button_id, _value.value);
1652
							}
1653
						},
1654
						title: egw.lang('Backup/Restore'),
1655
						buttons:[{"button_id": 'close',"text": egw.lang('Close'), id: 'dialog[close]', image: 'cancelled', "default":true}],
1656
						value: {
1657
							content: {
1658
								menu:_content
1659
							}
1660
						},
1661
						template: egw.webserverUrl+'/api/templates/default/pgp_backup_restore.xet',
1662
						class: "pgp_backup_restore",
1663
						modal:true
1664
			});
1665
		};
1666
		if (typeof mailvelope != 'undefined')
1667
		{
1668
			mailvelope.getKeyring('egroupware').then(function(_keyring)
1669
			{
1670
				self._mailvelopeBackupFileOperator(undefined, 'GET', function(_data){
1671
					dialog(menu);
1672
				},
1673
				function(){
1674
					// Remove delete item
1675
					menu.splice(3,1);
1676
					menu[3]['onclick'] = "app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', true)";
1677
					dialog(menu);
1678
				});
1679
			},
1680
			function(){
1681
				mailvelope.createKeyring('egroupware').then(function(){dialog(menu);});
1682
			});
1683
		}
1684
		else
1685
		{
1686
			this.mailvelopeInstallationOffer();
1687
		}
1688
	}
1689
1690
	/**
1691
	 * Create a dialog and offers installation option for installing mailvelope plugin
1692
	 * plus it offers a video tutorials to get the user morte familiar with mailvelope
1693
	 */
1694
	mailvelopeInstallationOffer ()
1695
	{
1696
		var buttons = [
1697
			{"text": egw.lang('Install'), id: 'install', image: 'check', "default":true},
1698
			{"text": egw.lang('Close'), id:'close', image: 'cancelled'}
1699
		];
1700
		var dialog = function(_content, _callback)
1701
		{
1702
			return et2_createWidget("dialog", {
1703
				callback: function(_button_id, _value) {
1704
					if (typeof _callback == "function")
1705
					{
1706
						_callback.call(this, _button_id, _value.value);
1707
					}
1708
				},
1709
				title: egw.lang('PGP Encryption Installation'),
1710
				buttons: buttons,
1711
				dialog_type: 'info',
1712
				value: {
1713
					content: _content
1714
				},
1715
				template: egw.webserverUrl+'/api/templates/default/pgp_installation.xet',
1716
				class: "pgp_installation",
1717
				modal: true
1718
				//resizable:false,
1719
			});
1720
		};
1721
		var content = [
1722
			// Header row should be empty item 0
1723
			{},
1724
			{domain:this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.',
1725
					'*.'+this._mailvelopeDomain()), video:"test", control:"true"}
1726
		];
1727
1728
		dialog(content, function(_button){
1729
			if (_button == 'install')
1730
			{
1731
				if (typeof chrome != 'undefined')
1732
				{
1733
					// ATM we are not able to trigger mailvelope installation directly
1734
					// since the installation should be triggered from the extension
1735
					// owner validate website (mailvelope.com), therefore, we just redirect
1736
					// user to chrome webstore to install mailvelope from there.
1737
					window.open('https://chrome.google.com/webstore/detail/mailvelope/kajibbejlbohfaggdiogboambcijhkke');
1738
				}
1739
				else if (typeof InstallTrigger != 'undefined' && InstallTrigger.enabled())
1740
				{
1741
					InstallTrigger.install({mailvelope:"https://download.mailvelope.com/releases/latest/mailvelope.firefox.xpi"},
1742
						function(_url, _status){
1743
							if (_status == 0)
1744
							{
1745
								et2_dialog.alert(egw.lang('Mailvelope addon installation succeded. Now you may configure the options.'));
1746
								return;
1747
							}
1748
							else
1749
							{
1750
								et2_dialog.alert(egw.lang('Mailvelope addon installation failed! Please try again.'));
1751
							}
1752
						});
1753
				}
1754
			}
1755
		});
1756
	}
1757
1758
	/**
1759
	 * PGP begin and end tags
1760
	 */
1761
	readonly begin_pgp_message: '-----BEGIN PGP MESSAGE-----';
1762
	readonly end_pgp_message: '-----END PGP MESSAGE-----';
1763
1764
	/**
1765
	 * Mailvelope "egroupware" Keyring
1766
	 */
1767
	mailvelope_keyring : any = undefined;
1768
1769
	/**
1770
	 * jQuery selector for Mailvelope iframes in all browsers
1771
	 */
1772
	readonly mailvelope_iframe_selector: 'iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]';
1773
1774
	/**
1775
	 * Open (or create) "egroupware" keyring and call callback with it
1776
	 *
1777
	 * @returns {Promise.<Keyring, Error>} Keyring or Error with message
1778
	 */
1779
	mailvelopeOpenKeyring()
1780
	{
1781
		var self = this;
1782
1783
		return new Promise(function(_resolve, _reject)
1784
		{
1785
			if (self.mailvelope_keyring) _resolve(self.mailvelope_keyring);
1786
1787
			var resolve = _resolve;
1788
			var reject = _reject;
1789
1790
			mailvelope.getKeyring('egroupware').then(function(_keyring)
1791
			{
1792
				self.mailvelope_keyring = _keyring;
1793
1794
				resolve(_keyring);
1795
				},
1796
			function(_err)
1797
			{
1798
				mailvelope.createKeyring('egroupware').then(function(_keyring)
1799
				{
1800
					self.mailvelope_keyring = _keyring;
1801
					var mvelo_settings_selector = self.mailvelope_iframe_selector
1802
						.split(',').map(function(_val){return 'body>'+_val;}).join(',');
1803
1804
					mailvelope.createSettingsContainer('body', _keyring, {
1805
						email: self.egw.user('account_email'),
1806
						fullName: self.egw.user('account_fullname')
1807
					}).then(function()
1808
					{
1809
						// make only Mailvelope settings dialog visible
1810
						jQuery(mvelo_settings_selector).css({position: 'absolute', top: 0});
1811
						// add a close button, so we know when to offer storing public key to AB
1812
						jQuery('<button class="et2_button et2_button_text" id="mailvelope_close_settings">'+self.egw.lang('Close')+'</button>')
1813
							.css({position: 'absolute', top: 8, right: 8, "z-index":2})
1814
							.click(function()
1815
							{
1816
								// try fetching public key, to check user created onw
1817
								self.mailvelope_keyring.exportOwnPublicKey(self.egw.user('account_email')).then(function(_pubKey)
1818
								{
1819
									// CreateBackupDialog
1820
									self.mailvelopeCreateBackupDialog().then(function(_popupId){
1821
										jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').css({position:'absolute', "z-index":1});
1822
									},
1823
									function(_err){
1824
										egw.message(_err);
1825
									});
1826
1827
									// if yes, hide settings dialog
1828
									jQuery(mvelo_settings_selector).each(function(index,item : any){
1829
										if (!item.src.match(/keyBackupDialog.html/,'ig')) item.remove();
1830
									});
1831
									jQuery('button#mailvelope_close_settings').remove();
1832
1833
									// offer user to store his public key to AB for other users to find
1834
									var buttons = [
1835
										{button_id: 2, text: 'Yes', id: 'dialog[yes]', image: 'check', default: true},
1836
										{button_id: 3, text : 'No', id: 'dialog[no]', image: 'cancelled'}
1837
									];
1838
									if (egw.user('apps').admin)
1839
									{
1840
										buttons.unshift({
1841
											button_id: 5, text: 'Yes and allow non-admin users to do that too (recommended)',
1842
											id: 'dialog[yes_allow]', image: 'check', default: true
1843
										});
1844
										delete buttons[1].default;
1845
									}
1846
									et2_dialog.show_dialog(function (_button_id)
1847
									{
1848
										if (_button_id != et2_dialog.NO_BUTTON )
1849
										{
1850
											var keys = {};
1851
											keys[self.egw.user('account_id')] = _pubKey;
1852
											self.egw.json('addressbook.addressbook_bo.ajax_set_pgp_keys',
1853
												[keys, _button_id != et2_dialog.YES_BUTTON ? true : undefined]).sendRequest()
1854
											.then(function(_data)
1855
											{
1856
												self.egw.message(_data.response['0'].data);
1857
											});
1858
										}
1859
									},
1860
									self.egw.lang('It is recommended to store your public key in addressbook, so other users can write you encrypted mails.'),
1861
									self.egw.lang('Store your public key in Addressbook?'),
1862
									{}, buttons, et2_dialog.QUESTION_MESSAGE, undefined, self.egw);
1863
								},
1864
								function(_err){
1865
									self.egw.message(_err.message+"\n\n"+
1866
									self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'error');
1867
								});
1868
							})
1869
							.appendTo('body');
1870
					});
1871
					resolve(_keyring);
1872
				},
1873
				function(_err)
1874
				{
1875
					reject(_err);
1876
				});
1877
			});
1878
		});
1879
	}
1880
1881
	/**
1882
	 * Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de"
1883
	 *
1884
	 * @returns {string}
1885
	 */
1886
	_mailvelopeDomain()
1887
	{
1888
		var parts = document.location.hostname.split('.');
1889
		if (parts.length > 1) parts.shift();
1890
		return parts.join('.');
1891
	}
1892
1893
	/**
1894
	 * Check if we have a key for all recipients
1895
	 *
1896
	 * @param {Array} _recipients
1897
	 * @returns {Promise.<Array, Error>} Array of recipients or Error with recipients without key
1898
	 */
1899
	mailvelopeGetCheckRecipients(_recipients)
1900
	{
1901
		// replace rfc822 addresses with raw email, as Mailvelop does not like them and lowercase all email
1902
		var rfc822_preg = /<([^'" <>]+)>$/;
1903
		var recipients = _recipients.map(function(_recipient)
1904
		{
1905
			var matches = _recipient.match(rfc822_preg);
1906
			return matches ? matches[1].toLowerCase() : _recipient.toLowerCase();
1907
		});
1908
1909
		// check if we have keys for all recipients
1910
		var self = this;
1911
		return new Promise(function(_resolve, _reject)
1912
		{
1913
			var resolve = _resolve;
1914
			var reject = _reject;
1915
			self.mailvelopeOpenKeyring().then(function(_keyring : any)
1916
			{
1917
				var keyring = _keyring;
1918
				_keyring.validKeyForAddress(recipients).then(function(_status)
1919
				{
1920
					var no_key = [];
1921
					for(var email in _status)
1922
					{
1923
						if (!_status[email]) no_key.push(email);
1924
					}
1925
					if (no_key.length)
1926
					{
1927
						// server addressbook on server for missing public keys
1928
						self.egw.json('addressbook.addressbook_bo.ajax_get_pgp_keys', [no_key]).sendRequest().then(function(_data)
1929
						{
1930
							var data = _data.response['0'].data;
1931
							var promises = [];
1932
							for(var email in data)
1933
							{
1934
								promises.push(keyring.importPublicKey(data[email]).then(function(_result)
1935
								{
1936
									if (_result == 'IMPORTED' || _result == 'UPDATED')
1937
									{
1938
										no_key.splice(no_key.indexOf(email),1);
1939
									}
1940
								}));
1941
							}
1942
							Promise.all(promises).then(function()
1943
							{
1944
								if (no_key.length)
1945
								{
1946
									reject(new Error(self.egw.lang('No key for recipient:')+' '+no_key.join(', ')));
1947
								}
1948
								else
1949
								{
1950
									resolve(recipients);
1951
								}
1952
							});
1953
						});
1954
					}
1955
					else
1956
					{
1957
						resolve(recipients);
1958
					}
1959
				});
1960
			},
1961
			function(_err)
1962
			{
1963
				reject(_err);
1964
			});
1965
		});
1966
	}
1967
1968
	/**
1969
	 * Check if the share action is enabled for this entry
1970
	 *
1971
	 * @param {egwAction} _action
1972
	 * @param {egwActionObject[]} _entries
1973
	 * @param {egwActionObject} _target
1974
	 * @returns {boolean} if action is enabled
1975
	 */
1976
	is_share_enabled(_action, _entries, _target)
1977
	{
1978
		return true;
1979
	}
1980
	/**
1981
	 * create a share-link for the given entry
1982
	 *
1983
	 * @param {egwAction} _action egw actions
1984
	 * @param {egwActionObject[]} _senders selected nm row
1985
	 * @param {egwActionObject} _target Drag source.  Not used here.
1986
	 * @param {Boolean} _writable Allow edit access from the share.
1987
	 * @param {Boolean} _files Allow access to files from the share.
1988
	 * @param {Function} _callback Callback with results
1989
	 * @returns {Boolean} returns false if not successful
1990
	 */
1991
	share_link(_action, _senders, _target, _writable, _files, _callback){
1992
		var path = _senders[0].id;
1993
		if(!path)
1994
		{
1995
			return this.egw.message(this.egw.lang('Missing share path.  Unable to create share.'), 'error');
1996
		}
1997
		switch(_action.id)
1998
		{
1999
			case 'shareFilemanager':
2000
				// Sharing a link to just files in filemanager
2001
				var id = path.split('::');
2002
				path = '/apps/'+ id[0] + '/' + id[1];
2003
		}
2004
		if(typeof _writable === 'undefined' && _action.parent && _action.parent.getActionById('shareWritable'))
2005
		{
2006
			_writable = _action.parent.getActionById('shareWritable').checked || false;
2007
		}
2008
		if(typeof _files === 'undefined' && _action.parent && _action.parent.getActionById('shareFiles'))
2009
		{
2010
			_files = _action.parent.getActionById('shareFiles').checked || false;
2011
		}
2012
2013
		return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files],
2014
			_callback ? _callback : this._share_link_callback, this, true, this).sendRequest();
2015
	}
2016
2017
	share_merge(_action, _senders, _target)
2018
	{
2019
		var parent = _action.parent.parent;
2020
		var _writable = false;
2021
		var _files = false;
2022
		if(parent && parent.getActionById('shareWritable'))
2023
		{
2024
			_writable = parent.getActionById('shareWritable').checked || false;
2025
		}
2026
		if(parent && parent.getActionById('shareFiles'))
2027
		{
2028
			_files = parent.getActionById('shareFiles').checked || false;
2029
		}
2030
2031
		// Share only works on one at a time
2032
		var promises = [];
2033
		for(var i = 0; i < _senders.length; i++)
2034
		{
2035
			promises.push(new Promise(function(resolve, reject) {
2036
				this.share_link(_action, [_senders[i]], _target, _writable, _files, resolve);
2037
			}.bind(this)));
2038
		}
2039
2040
		// But merge into email can handle several
2041
		Promise.all(promises.map(function(p){p.catch(function(e){console.log(e)})}))
2042
			.then(function(values) {
2043
				// Process document after all shares created
2044
				return nm_action(_action, _senders, _target);
2045
			});
2046
	}
2047
2048
	/**
2049
	 * Share-link callback
2050
	 * @param {object} _data
2051
	 */
2052
	_share_link_callback(_data) {
2053
		if (_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname);
2054
2055
		var copy_link_to_clipboard = function(evt){
2056
			var $target = jQuery(evt.target);
2057
			$target.select();
2058
			try {
2059
				var successful = document.execCommand('copy');
2060
				if (successful)
2061
				{
2062
					egw.message('Share link copied into clipboard');
2063
					return true;
2064
				}
2065
			}
2066
			catch (e) {}
2067
			egw.message('Failed to copy the link!');
2068
		};
2069
		jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard);
2070
		et2_createWidget("dialog", {
2071
			callback: function( button_id, value) {
2072
				jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard);
2073
				return true;
2074
			},
2075
			title: _data.title ? _data.title : egw.lang("%1 Share Link", _data.writable ? egw.lang("Writable"): egw.lang("Readonly")),
2076
			template: _data.template,
2077
			width: 450,
2078
			value: {content:{ "share_link": _data.share_link }}
2079
		});
2080
	}
2081
}
2082